"""
The only sensible way to deal with sockets is in a thread.
The only safe way to deal with communication between threads is via a Queue.
This pcakage encapsulates the creation of clients and servers and provides
reliable communication via a queue.

This IO class supports 3 uses:
   a) a simple client
   b) a server that starts several clients
   c) a client started by a server

This version sends messages with embeded length.
Each message has a tag to indicate the type of message.
The tag is used to dispatch the message handler to a method.
Tags and messages are strings.
The message lengths are encoded as network byte order integers.
"""

import os, socket, time, threading
from struct import pack, unpack
from Queue import Queue, Empty

class _IO2(object):
    """
    NOTE: this class should not be used directly see Client2 and Server2 below.
    A general IO class providing safe message passing using sockets.
    User Api is:
        active (to see if the socket is connected)
        dispatch (to handle any messages)
        put (to send a message)
        quit (to close the socket)
    Messages are passed via a queue to a user method.
    Messages are strings.
    Each message has a tag that is used to identify the handling method.
    """

    def __init__(self, name, socket=None, queue=None):
        """
        Create thread with name, socket and queue.
        If socket is None then caller must provide _connect method
        If queue is None one will be created
        """
        self._name = name
        print '%s created' % self
        self._socket = socket
        self._queue = queue or Queue()
        self._working = False
        t = threading.Thread(target=self._work)
        t.setDaemon(True)
        t.start()

    @property
    def active(self):
        """
        """
        return self._working

    def __repr__(self):
        """
        """
        return self._name

    def _put1(self, msg, mlen):
        """
        Low level send via socket.
        """
        total = 0
        while total<mlen:
            try:
                sent = self._socket.send(msg[total:])
            except Exception, emsg:
                self.quit('write %s\n\texception %s' % (msg, emsg))
                return
            if sent==0:
                self.quit('write %s\n\tfail' % msg)
                return
            total += sent
        return total

    def _put2(self, msg):
        """
        msg::<length of msg><msg>
        length is network byte order 4 byte integer
        """
        if not isinstance(msg, basestring):
            msg = str(msg)
        mlen = len(msg)
        return self._put1(pack('!i', mlen),4) and self._put1(msg, mlen)

    def put(self, tag, msg):
        """
        Send a tag and msg via the socket.
        """
        self._put2(tag) and self._put2(msg)

    def _qget(self):
        """
        Low level get from queue (if there is anything).
        """
        try:
            return self._queue.get(block=False)
        except Empty, emsg:
            pass

    def dispatch(self):
        """
        Receive and dispatch all messages from the queue.
        call self.<tag>(socket, msg) or self.tag_<1234>(socket, msg)
        """
        while True:
            msg = self._qget()
            if not msg:
                return
            socket, tag, msg = msg
            if tag.isdigit():
                tag = 'tag_%s' % tag
            if hasattr(self, tag):
                getattr(self, tag)(socket, msg)
            else:
                print '%s missing method:%s\n\tmessage:%s' % (self, tag, msg)

    def quit(self, reason=None):
        """
        Its all over.
        """
        if reason:
            print '%s quit reason:%s' % (self, reason)
        self._working = False

    def _qput(self, tag, msg):
        """
        Send a message through the queue.
        Pass self so receiver can reply.
        """
        self._queue.put((self,tag,msg))

    def _connect(self):
        """
        The default is already connected.
        """
        return self._socket

    def _read1(self, nbytes):
        """
        Low level socket read given a number of bytes.
        """
        s = self._socket
        result = []
        rbytes = 0
        while rbytes<nbytes:
            try:
                bytes = s.recv(nbytes-rbytes)
            except Exception, emsg:
                self.quit('read exception %s' % emsg)
                return
            if bytes=='':
                self.quit('read no bytes')
                return
            rbytes += len(bytes)
            result.append(bytes)
        return ''.join(result)

    def _read2(self):
        """
        Read the 4 byte network byte order length of the message, then the message.
        """
        msglen = self._read1(4)
        if msglen:
            return self._read1(unpack('!i', msglen)[0])

    def _work(self):
        """
        This is where the work happens...
        Connect, read loop, disconnect.
        Pass messages into the queue.
        """
        s = self._connect()
        if not s:
            return
        s.setblocking(1)
        qput = self._qput
        read = self._read2
        self._working = True
        while self._working:
            tag = read()
            if not tag:
                break
            msg = read()
            if not msg:
                break
            qput(tag, msg)
        s.close()

class Client2(_IO2):
    """
    An IO class for a client.
    """

    def __init__(self, name, host, port):
        """
        """
        self._address = (host, port)
        _IO2.__init__(self, name)

    def _connect(self):
        """
        Wait for a server.
        """
        print '%s waiting for server at %s' %  (self, self._address)
        while True:
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            try:
                s.connect(self._address)
                self._socket = s
                return s
            except socket.error, emsg:
                time.sleep(1.0)

class Server2(_IO2):
    """
    An IO class for a server.
    """
    _count = 0 # incrementing client count

    def __init__(self, name, host, port):
        """
        """
        self._address = (host, port)
        _IO2.__init__(self, name)
        self._sockets = [] # list of sockets

    @property
    def opensockets(self):
        """
        """
        for s in self._sockets:
            if s.active:
                yield s

    def _work(self):
        """
        A servers job is to start client threads as they arrive.
        Clients are given a socket and use the server queue.
        """
        print '%s serving at %s' % (self, self._address)
        _socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        _socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        _socket.bind(self._address)
        _socket.listen(10)
        self._working = True
        while self._working:
            Server2._count += 1
            (clientsock, _) = _socket.accept()
            self._sockets.append(_IO2('%s-client-%s' % (self, Server2._count), clientsock, self._queue))
